// SPDX-License-Identifier: GPL-2.0
/*
 * lombo-emu-gsensor.c - Lombo gpadc driver module
 *
 * Copyright (C) 2016-2020, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 */

#include <linux/device.h>
#include <linux/err.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/of_platform.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/iio/events.h>
#include <linux/iio/buffer.h>
#include <linux/iio/trigger.h>
#include <linux/input.h>
#include <linux/random.h>

#include <mach/debug.h>

#define DRIVER_NAME			"lombo-emu-gsensor"
#define INPUT_DEV_NAME		"emu-gsensor"

#define MAX_VALUE		4096

enum ATTR_TYPE {
	ATTR_X = 0,
	ATTR_Y,
	ATTR_Z,
};

enum GS_CMD_TYPE {
	GS_CMD_SHOW = 0,
	GS_CMD_REPORT,
	GS_CMD_RAND,
};

struct lombo_emu_gs_device {
	int x;
	int y;
	int z;
	int enable;
	struct input_dev *gs_input_dev;
};

static int lombo_gs_read_chan(
		struct lombo_emu_gs_device *gs_dev,
		struct iio_chan_spec const *chan)
{
	int val;
	int ch;

	if (chan->type == IIO_ACCEL) {
		ch = chan->channel2;
		switch (ch) {
		case IIO_MOD_X:
			val = gs_dev->x;
			break;
		case IIO_MOD_Y:
			val = gs_dev->y;
			break;
		case IIO_MOD_Z:
			val = gs_dev->z;
			break;
		default:
			pr_warn("unknown IIO_VOLTAGE channel: %d\n", ch);
			return -1;
		}
	} else {
		pr_warn("unknown chan type: %d\n", chan->type);
		return -1;
	}

	return val;
}

static void lombo_gs_report_event(struct lombo_emu_gs_device *gs_dev)
{
	struct input_dev *dev = gs_dev->gs_input_dev;

	if (!dev) {
		pr_err("input device is NULL.");
		return;
	}

	pr_info("gsensor report event -> x: %d, y: %d, z: %d\n",
			gs_dev->x, gs_dev->y, gs_dev->z);
	input_report_abs(dev, ABS_X, gs_dev->x);
	input_report_abs(dev, ABS_Y, gs_dev->y);
	input_report_abs(dev, ABS_Z, gs_dev->z);
	input_sync(dev);
}

static void lombo_gs_log_xyz(struct lombo_emu_gs_device *gs_dev)
{
	pr_info("gsensor value -> x: %d, y: %d, z: %d\n",
			gs_dev->x, gs_dev->y, gs_dev->z);
}

static void lombo_gs_rand_xyz(struct lombo_emu_gs_device *gs_dev)
{
	int x = get_random_u32() % MAX_VALUE;
	int y = get_random_u32() % MAX_VALUE;
	int z = get_random_u32() % MAX_VALUE;

	gs_dev->x = x;
	gs_dev->y = y;
	gs_dev->z = z;

	pr_info("generate random x y z ...");
	lombo_gs_log_xyz(gs_dev);
}

static int lombo_gs_read_raw(struct iio_dev *indio_dev,
			      struct iio_chan_spec const *chan,
			      int *val, int *val2, long mask)
{
	struct lombo_emu_gs_device *gs_dev = iio_priv(indio_dev);

	pr_info("chan channel: %d, name: %s, type: %d\n",
			chan->channel2, chan->datasheet_name, chan->type);

	switch (mask) {
	case IIO_CHAN_INFO_RAW:
		*val = lombo_gs_read_chan(gs_dev, chan);
		if (*val < 0)
			return *val;

		return IIO_VAL_INT;

	default:
		pr_warn("%s unknown mask(%ld)\n", __func__, mask);
		break;
	}

	return -EINVAL;
}

static ssize_t __cpy_value_to_buf(char *buf, int val)
{
	int len;

	len = sprintf(buf, "%d\n", val);
	return len;
}

static ssize_t __show_value(struct device *dev,
							char *buf,
							enum ATTR_TYPE t)
{
	struct lombo_emu_gs_device *gs_dev = iio_priv(dev_to_iio_dev(dev));
	int v;

	switch (t) {
	case ATTR_X:
		v = gs_dev->x;
		break;
	case ATTR_Y:
		v = gs_dev->y;
		break;
	case ATTR_Z:
		v = gs_dev->z;
		break;
	default:
		pr_warn("%s unknown ATTR_TYPE(%u)\n", __func__, t);
		return -EINVAL;
	}

	return __cpy_value_to_buf(buf, v);
}

static ssize_t __store_value(struct device *dev,
							const char *buf,
							size_t count,
							enum ATTR_TYPE t)
{
	int value;
	struct lombo_emu_gs_device *gs_dev = iio_priv(dev_to_iio_dev(dev));

	if (kstrtoint(buf, 0, &value))
		return -EINVAL;

	switch (t) {
	case ATTR_X:
		gs_dev->x = value;
		break;
	case ATTR_Y:
		gs_dev->y = value;
		break;
	case ATTR_Z:
		gs_dev->z = value;
		break;
	default:
		pr_warn("%s unknown ATTR_TYPE(%u)\n", __func__, t);
		return -EINVAL;
	}

	return count;
}

static ssize_t gsensor_x_show(struct device *dev,
			struct device_attribute *attr, char *buf)
{
	return __show_value(dev, buf, ATTR_X);
}

static ssize_t gsensor_x_store(struct device *dev,
				struct device_attribute *attr,
				const char *buf, size_t count)
{
	return __store_value(dev, buf, count, ATTR_X);
}

static ssize_t gsensor_y_show(struct device *dev,
			struct device_attribute *attr, char *buf)
{
	return __show_value(dev, buf, ATTR_Y);
}

static ssize_t gsensor_y_store(struct device *dev,
				struct device_attribute *attr,
				const char *buf, size_t count)
{
	return __store_value(dev, buf, count, ATTR_Y);
}

static ssize_t gsensor_z_show(struct device *dev,
			struct device_attribute *attr, char *buf)
{
	return __show_value(dev, buf, ATTR_Z);
}

static ssize_t gsensor_z_store(struct device *dev,
				struct device_attribute *attr,
				const char *buf, size_t count)
{
	return __store_value(dev, buf, count, ATTR_Z);
}

static ssize_t enable_show(struct device *dev,
			struct device_attribute *attr, char *buf)
{
	struct lombo_emu_gs_device *gs_dev = iio_priv(dev_to_iio_dev(dev));

	return __cpy_value_to_buf(buf, gs_dev->enable);
}

static ssize_t enable_store(struct device *dev,
				struct device_attribute *attr,
				const char *buf, size_t count)
{
	int value;
	struct lombo_emu_gs_device *gs_dev = iio_priv(dev_to_iio_dev(dev));

	if (kstrtoint(buf, 0, &value))
		return -EINVAL;

	gs_dev->enable = value;
	return count;
}

static ssize_t cmd_store(struct device *dev,
				struct device_attribute *attr,
				const char *buf, size_t count)
{
	int value;
	enum GS_CMD_TYPE t;
	struct lombo_emu_gs_device *gs_dev = iio_priv(dev_to_iio_dev(dev));

	if (kstrtoint(buf, 0, &value))
		return -EINVAL;

	t = (enum GS_CMD_TYPE)value;
	switch (t) {
	case GS_CMD_SHOW:
		lombo_gs_log_xyz(gs_dev);
		break;
	case GS_CMD_REPORT:
		lombo_gs_report_event(gs_dev);
		break;
	case GS_CMD_RAND:
		lombo_gs_rand_xyz(gs_dev);
		break;
	default:
		pr_warn("%s unknown GS_CMD_TYPE(%u)\n", __func__, t);
		return -EINVAL;
	}

	return count;
}

static IIO_DEVICE_ATTR_RW(gsensor_x, 0);
static IIO_DEVICE_ATTR_RW(gsensor_y, 0);
static IIO_DEVICE_ATTR_RW(gsensor_z, 0);
//static IIO_DEVICE_ATTR_RW(enable, 0);
static IIO_DEVICE_ATTR_WO(cmd, 0);
static IIO_DEVICE_ATTR(enable, 0220, enable_show, enable_store, 0);

static struct attribute *gs_attributes[] = {
	&iio_dev_attr_gsensor_x.dev_attr.attr,
	&iio_dev_attr_gsensor_y.dev_attr.attr,
	&iio_dev_attr_gsensor_z.dev_attr.attr,
	&iio_dev_attr_enable.dev_attr.attr,
	&iio_dev_attr_cmd.dev_attr.attr,
	NULL,
};

static const struct attribute_group gs_attrs_group = {
	.attrs = gs_attributes,
};

static const struct iio_info lombo_iio_info = {
	.attrs = &gs_attrs_group,
	.read_raw = lombo_gs_read_raw,
};


/* {direction}_{type}_{modifier}_{info_mask} */
#define LOMBO_GS_ACC_CHANNEL(_channel, _name)		\
	{							\
		.type = IIO_ACCEL,					\
		.modified = 1,					\
		.channel2 = _channel,				\
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),	\
		.datasheet_name = _name,			\
	}


/* Gsensor x y z channel */
static const struct iio_chan_spec lombo_gs_channels[] = {
	LOMBO_GS_ACC_CHANNEL(IIO_MOD_X, "x"),
	LOMBO_GS_ACC_CHANNEL(IIO_MOD_Y, "y"),
	LOMBO_GS_ACC_CHANNEL(IIO_MOD_Z, "z"),
};

static int lombo_gs_probe(struct platform_device *pdev)
{
	int ret;
	struct lombo_emu_gs_device *gs_dev;
	struct iio_dev *indio_dev;

	indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(*gs_dev));
	if (!indio_dev)
		return -ENOMEM;

	platform_set_drvdata(pdev, indio_dev);
	gs_dev = iio_priv(indio_dev);

	indio_dev->name = dev_name(&pdev->dev);
	indio_dev->dev.parent = &pdev->dev;
	indio_dev->info = &lombo_iio_info;
	indio_dev->modes = INDIO_DIRECT_MODE;
	indio_dev->channels = lombo_gs_channels;
	indio_dev->num_channels = ARRAY_SIZE(lombo_gs_channels);

	ret = iio_device_register(indio_dev);
	if (ret) {
		dev_err(&pdev->dev, "unable to register device\n");
		return ret;
	}

	/* input device for report event */
	gs_dev->gs_input_dev = input_allocate_device();
	if (!gs_dev->gs_input_dev) {
		dev_err(&pdev->dev, "input_allocate_device failed");
		ret = -ENOMEM;
		goto err_out;
	}

	/* report x y z value */
	gs_dev->gs_input_dev->name = INPUT_DEV_NAME;
	set_bit(EV_ABS, gs_dev->gs_input_dev->evbit);
	set_bit(ABS_X, gs_dev->gs_input_dev->keybit);
	set_bit(ABS_Y, gs_dev->gs_input_dev->keybit);
	set_bit(ABS_Z, gs_dev->gs_input_dev->keybit);

	input_set_abs_params(gs_dev->gs_input_dev, ABS_X,
						0, MAX_VALUE, 0, 0);
	input_set_abs_params(gs_dev->gs_input_dev, ABS_Y,
						0, MAX_VALUE, 0, 0);
	input_set_abs_params(gs_dev->gs_input_dev, ABS_Z,
						0, MAX_VALUE, 0, 0);

	ret = input_register_device(gs_dev->gs_input_dev);
	if (ret) {
		dev_err(&pdev->dev, "input_register_device failed");
		goto err_out;
	}

	return 0;

err_out:
	iio_device_unregister(indio_dev);
	return ret;
}

static int lombo_gs_remove(struct platform_device *pdev)
{
	struct iio_dev *indio_dev = platform_get_drvdata(pdev);
	struct lombo_emu_gs_device *gs_dev = iio_priv(indio_dev);

	input_unregister_device(gs_dev->gs_input_dev);
	input_free_device(gs_dev->gs_input_dev);

	iio_device_unregister(indio_dev);

	return 0;
}

static int lombo_gs_suspend(struct platform_device *pdev,
		pm_message_t state)
{
	return 0;
}

static int lombo_gs_resume(struct platform_device *pdev)
{
	return 0;
}

#ifdef CONFIG_OF
static const struct of_device_id lombo_gsenor_match[] = {
	{ .compatible = "lombo,emulated-gsensor" },
	{},
};
MODULE_DEVICE_TABLE(of, lombo_gsenor_match);
#endif


static struct platform_driver lombo_gs_driver = {
	.probe		= lombo_gs_probe,
	.remove		= lombo_gs_remove,
	.suspend	= lombo_gs_suspend,
	.resume		= lombo_gs_resume,
	.driver		= {
		.name	= DRIVER_NAME,
		.owner	= THIS_MODULE,
	#ifdef CONFIG_OF
		.of_match_table = of_match_ptr(lombo_gsenor_match),
	#endif
	},
};
module_platform_driver(lombo_gs_driver);

MODULE_DESCRIPTION("LomboTech emulated gsensor driver");
MODULE_AUTHOR("lomboswer@lombotech.com");
MODULE_LICENSE("GPL");

